package org.codefilarete.stalactite.mapping;

import java.util.function.Function;

import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
import org.codefilarete.stalactite.engine.PersistExecutor.DefaultPersistExecutor.DefaultIsNewDeterminer;
import org.codefilarete.stalactite.mapping.id.assembly.SingleIdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.manager.AlreadyAssignedIdentifierManager;
import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
import org.codefilarete.stalactite.sql.ddl.structure.Table;

/**
 * Entry point for single value (hence single-column primary key), as opposed to composed, about entity identifier mapping.
 * Will mainly delegate its work to an {@link IdAccessor}, an {@link IdentifierInsertionManager} and a {@link SingleIdentifierAssembler}
 * 
 * @author Guillaume Mary
 * @see ComposedIdMapping
 */
public class SimpleIdMapping<C, I> implements IdMapping<C, I> {
	
	private final AccessorWrapperIdAccessor<C, I> idAccessor;
	
	private final IdentifierInsertionManager<C, I> identifierInsertionManager;
	
	private final DefaultIsNewDeterminer<C> isNewDeterminer;
	
	private final SingleIdentifierAssembler<I, ?> identifierMarshaller;
	
	/**
	 * Main constructor
	 * 
	 * @param idAccessor entry point to get/set id of an entity
	 * @param identifierInsertionManager defines the way the id is persisted into the database
	 * @param identifierMarshaller defines the way the id is read from the database
	 */
	public SimpleIdMapping(AccessorWrapperIdAccessor<C, I> idAccessor,
						   IdentifierInsertionManager<C, I> identifierInsertionManager,
						   SingleIdentifierAssembler<I, ?> identifierMarshaller) {
		this.idAccessor = idAccessor;
		this.identifierInsertionManager = identifierInsertionManager;
		this.identifierMarshaller = identifierMarshaller;
		if (identifierInsertionManager instanceof AlreadyAssignedIdentifierManager) {
			this.isNewDeterminer = new AlreadyAssignedIdDeterminer(((AlreadyAssignedIdentifierManager<C, I>) identifierInsertionManager).getIsPersistedFunction());
		} else if (identifierInsertionManager.getIdentifierType().isPrimitive()) {
			this.isNewDeterminer = new PrimitiveIdDeterminer();
		} else {
			this.isNewDeterminer = new NullableIdDeterminer();
		}
	}
	
	public SimpleIdMapping(ReadWritePropertyAccessPoint<C, I> identifierAccessor,
						   IdentifierInsertionManager<C, I> identifierInsertionManager,
						   SingleIdentifierAssembler identifierMarshaller) {
		this(new AccessorWrapperIdAccessor<>(identifierAccessor), identifierInsertionManager, identifierMarshaller);
	}
	
	@Override
	public AccessorWrapperIdAccessor<C, I> getIdAccessor() {
		return idAccessor;
	}
	
	@Override
	public IdentifierInsertionManager<C, I> getIdentifierInsertionManager() {
		return identifierInsertionManager;
	}
	
	@Override
	public boolean isNew(C entity) {
		return isNewDeterminer.isNew(entity);
	}
	
	@Override
	public <T extends Table<T>> SingleIdentifierAssembler<I, T> getIdentifierAssembler() {
		return (SingleIdentifierAssembler<I, T>) identifierMarshaller;
	}
	
	/**
	 * For case where the identifier is a basic type (String, Long, ...)
	 */
	private class NullableIdDeterminer extends DefaultIsNewDeterminer<C> {
		
		@Override
		public boolean isNew(C entity) {
			return idAccessor.getId(entity) == null;
		}
	}
	
	/**
	 * For case where the identifier is a primitive type (long, int, ...)
	 */
	private class PrimitiveIdDeterminer extends DefaultIsNewDeterminer<C> {
		
		@Override
		public boolean isNew(C entity) {
			return ((Number) idAccessor.getId(entity)).intValue() == 0;
		}
	}
	
	/**
	 * For case where the identifier is already assigned : we have to delegate determination to a function
	 */
	private class AlreadyAssignedIdDeterminer extends DefaultIsNewDeterminer<C> {
		
		private final Function<C, Boolean> isPersistedFunction;
		
		private AlreadyAssignedIdDeterminer(Function<C, Boolean> isPersistedFunction) {
			this.isPersistedFunction = isPersistedFunction;
		}
		
		@Override
		public boolean isNew(C entity) {
			return !isPersistedFunction.apply(entity);
		}
	}
}
